/**
 * $RCSfile$
 * $Revision: $
 * $Date: $
 *
 * Copyright 2003-2006 Jive Software.
 *
 * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.jivesoftware.smackx.filetransfer;

import org.jivesoftware.smack.XMPPException;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

/**
 * Contains the generic file information and progress related to a particular
 * file transfer.
 *
 * @author Alexander Wenckus
 *
 */
public abstract class FileTransfer {

    private String fileName;
    private String filePath;
    private long fileSize;
    private String peer;
    private Status status = Status.initial;
    private final Object statusMonitor = new Object();
    protected FileTransferNegotiator negotiator;
    protected String streamID;
    protected long amountWritten = -1;
    private Error error;
    private Exception exception;
    /**
     * Buffer size between input and output
     */
    private static final int BUFFER_SIZE = 8092;

    private static int ftCurrentCount = 1;
    private int ftCounter = 0;

    protected FileTransfer(String peer, String streamID,
            FileTransferNegotiator negotiator) {
        this.peer = peer;
        this.streamID = streamID;
        this.negotiator = negotiator;
        this.ftCounter = ftCurrentCount++;
    }

    protected void setFileInfo(String fileName, long fileSize) {
        this.fileName = fileName;
        this.fileSize = fileSize;
    }

    protected void setFileInfo(String path, String fileName, long fileSize) {
        this.filePath = path;
        this.fileName = fileName;
        this.fileSize = fileSize;
    }

    /**
     * Returns the size of the file being transfered.
     *
     * @return Returns the size of the file being transfered.
     */
    public long getFileSize() {
        return fileSize;
    }

    /**
     * Returns the name of the file being transfered.
     *
     * @return Returns the name of the file being transfered.
     */
    public String getFileName() {
        return fileName;
    }

    /**
     * Returns the local path of the file.
     *
     * @return Returns the local path of the file.
     */
    public String getFilePath() {
        return filePath;
    }

    /**
     * Returns the JID of the peer for this file transfer.
     *
     * @return Returns the JID of the peer for this file transfer.
     */
    public String getPeer() {
        return peer;
    }

    /**
     * Returns the progress of the file transfer as a number between 0 and 1.
     *
     * @return Returns the progress of the file transfer as a number between 0
     *         and 1.
     */
    public double getProgress() {
        if (amountWritten <= 0 || fileSize <= 0) {
            return 0;
        }
        return (double) amountWritten / (double) fileSize;
    }

    /**
     * Returns true if the transfer has been cancelled, if it has stopped because
     * of a an error, or the transfer completed succesfully.
     *
     * @return Returns true if the transfer has been cancelled, if it has stopped
     *         because of a an error, or the transfer completed succesfully.
     */
    public boolean isDone() {
        return status == Status.cancelled || status == Status.error
                || status == Status.complete || status == Status.refused;
    }

    /**
     * Retuns the current status of the file transfer.
     *
     * @return Retuns the current status of the file transfer.
     */
    public Status getStatus() {
        return status;
    }

    protected void setError(Error type) {
        this.error = type;
    }

    /**
     * When {@link #getStatus()} returns that there was an {@link Status#error}
     * during the transfer, the type of error can be retrieved through this
     * method.
     *
     * @return Returns the type of error that occured if one has occured.
     */
    public Error getError() {
        return error;
    }

    /**
     * If an exception occurs asynchronously it will be stored for later
     * retrival. If there is an error there maybe an exception set.
     *
     * @return The exception that occured or null if there was no exception.
     * @see #getError()
     */
    public Exception getException() {
        return exception;
    }

    public String getStreamID() {
        return streamID;
    }

    /**
     * Cancels the file transfer.
     */
    public abstract void cancel();

    protected void setException(Exception exception) {
        this.exception = exception;
    }

    protected void setStatus(Status status) {
        synchronized (statusMonitor) {
            this.status = status;
        }
    }

    protected boolean updateStatus(Status oldStatus, Status newStatus) {
        synchronized (statusMonitor) {
            if (oldStatus != status) {
                return false;
            }
            status = newStatus;
            return true;
        }
    }

    //public void oddMethod() {
    //    System.out.println("FileTransfer.oddMethod()");
    //}

    protected void writeToStream(final InputStream in, final OutputStream out)
            throws XMPPException {
        System.out.println("FileTransfer.writeToStream()");
        final byte[] b = new byte[BUFFER_SIZE];
        int count = 0;
        amountWritten = 0;

        do {
            // write to the output stream
            try {
                //oddMethod();
                out.write(b, 0, count);
                out.flush();
                //System.out.println("FileTransfer.writeToStream() out.write() ftCounter " + ftCounter);
                //System.out.write(b, 0, count);

            } catch (IOException e) {
                throw new XMPPException("error writing to output stream", e);
            }

            amountWritten += count;

            // read more bytes from the input stream
            try {
                count = in.read(b);
                //System.out.println("FileTransfer.writeToStream() in.read() " + count + " bytes ftCounter " + ftCounter);
            } catch (IOException e) {
                throw new XMPPException("error reading from input stream", e);
            }

        } while (count != -1 && !getStatus().equals(Status.cancelled));

        //if ((count == -1 || getStatus().equals(Status.cancelled))) {
        //    System.out.println("FileTransfer.writeToStream() cancelled... count? " + count + " ftCounter " + ftCounter);
        //}

        // the connection was likely terminated abrubtly if these are not equal
        if (!getStatus().equals(Status.cancelled) && getError() == Error.none
                && amountWritten != fileSize) {
            setStatus(Status.error);
            this.error = Error.connection;
        }
    }

    /**
     * A class to represent the current status of the file transfer.
     *
     * @author Alexander Wenckus
     *
     */
    public static class /*enum*/ Status {

        /**
         * An error occured during the transfer.
         *
         * @see FileTransfer#getError()
         */
        public static final Status error = new Status("Error");
        /**
         * The initial status of the file transfer.
         */
        public static final Status initial = new Status("Initial");
        /**
         * The file transfer is being negotiated with the peer. The party
         * recieving the file has the option to accept or refuse a file transfer
         * request. If they accept, then the process of stream negotiation will
         * begin. If they refuse the file will not be transfered.
         *
         * @see #negotiating_stream
         */
        public static final Status negotiating_transfer = new Status("Negotiating Transfer");
        /**
         * The peer has refused the file transfer request halting the file
         * transfer negotiation process.
         */
        public static final Status refused = new Status("Refused");
        /**
         * The stream to transfer the file is being negotiated over the chosen
         * stream type. After the stream negotiating process is complete the
         * status becomes negotiated.
         *
         * @see #negotiated
         */
        public static final Status negotiating_stream = new Status("Negotiating Stream");
        /**
         * After the stream negotitation has completed the intermediate state
         * between the time when the negotiation is finished and the actual
         * transfer begins.
         */
        public static final Status negotiated = new Status("Negotiated");
        /**
         * The transfer is in progress.
         *
         * @see FileTransfer#getProgress()
         */
        public static final Status in_progress = new Status("In Progress");
        /**
         * The transfer has completed successfully.
         */
        public static final Status complete = new Status("Complete");
        /**
         * The file transfer was canceled
         */
        public static final Status cancelled = new Status("Cancelled");
        private String status;

        private Status(String status) {
            this.status = status;
        }

        public String toString() {
            return status;
        }
    }

    /**
     * Return the length of bytes written out to the stream.
     * @return the amount in bytes written out.
     */
    public long getAmountWritten() {
        return amountWritten;
    }

    public static class /*enum*/ Error {

        /**
         * No error
         */
        public static final Error none = new Error("No error");
        /**
         * The peer did not find any of the provided stream mechanisms
         * acceptable.
         */
        public static final Error not_acceptable = new Error("The peer did not find any of the provided stream mechanisms acceptable.");
        /**
         * The provided file to transfer does not exist or could not be read.
         */
        public static final Error bad_file = new Error("The provided file to transfer does not exist or could not be read.");
        /**
         * The remote user did not respond or the connection timed out.
         */
        public static final Error no_response = new Error("The remote user did not respond or the connection timed out.");
        /**
         * An error occured over the socket connected to send the file.
         */
        public static final Error connection = new Error("An error occured over the socket connected to send the file.");
        /**
         * An error occured while sending or recieving the file
         */
        public static final Error stream = new Error("An error occured while sending or recieving the file.");
        private final String msg;

        private Error(String msg) {
            this.msg = msg;
        }

        /**
         * Returns a String representation of this error.
         *
         * @return Returns a String representation of this error.
         */
        public String getMessage() {
            return msg;
        }

        public String toString() {
            return msg;
        }
    }
}
